package com.dxtan90.strategy;

import cn.hutool.core.io.FileUtil;
import com.dxtan90.util.CommonUtil;
import com.dxtan90.util.PropertiesUtil;
import com.dxtan90.util.SyncMission;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * 爬取策略基本实现类
 *
 * @author shijunhao
 * @since 2024/4/16 11:21
 */
public abstract class BaseStrategy implements IFindStrategy, IParseStrategy {

    protected static final Logger logger = LoggerFactory.getLogger(BaseStrategy.class);

    /**
     * 日志路径
     */
    public static final String LOG_PATH = PropertiesUtil.getProperty("novel.target.logPath");
    /**
     * 成果路径
     */
    public static final String BOOK_PATH = PropertiesUtil.getProperty("novel.target.bookPath");
    /**
     * 资源路径
     */
    public String SOURCE_URL;
    /**
     * 目录地址（最后一级）
     */
    public String CATEGORY_PATH = PropertiesUtil.getProperty("novel.target.categoryPath");
    /**
     * 过滤文章信息的正则表达式
     */
    public static final Set<String> FILTER_REGEX_SET = new HashSet<>();
    /**
     * 过滤文章信息的正则匹配器
     */
    public static final List<Pattern> FILTER_REGEX_PATTERN_SET = new ArrayList<>();
    /**
     * 异步线程池
     */
    protected final ExecutorService EXECUTOR = new ThreadPoolExecutor(
            8, // 核心线程数
            16, // 最大线程数
            0L, // 空闲线程的存活时间
            TimeUnit.MILLISECONDS, // 存活时间的时间单位
            new LinkedBlockingQueue<>() // 无界队列，用于存放任务
    );

    public BaseStrategy() {
        SOURCE_URL = getSourceUrl();
        CATEGORY_PATH = CATEGORY_PATH + getCategoryName();
        FileUtil.mkdir(CATEGORY_PATH);
        addFilterRegex();
        initPattern();
    }

    @Override
    public void getNovel(String novelUrl) throws IOException {
        String novelName = this.getCategory(novelUrl);
        String categoryPath = CATEGORY_PATH + "/" + novelName + ".txt";
        try {
            List<String> sections = Files.lines(Paths.get(categoryPath))
                    .collect(Collectors.toList());
            this.parseSectionList(sections, novelName, false);
        } catch (IOException e) {
            logger.error("读取目录文件失败：[{}]", categoryPath);
        }
    }

    @Override
    public void getRecentUpdate(String novelName, String categoryUrl) {
        String categoryPath = CATEGORY_PATH + "/" + novelName + ".txt";
        try {
            List<String> sections = Files.lines(Paths.get(categoryPath))
                    .collect(Collectors.toList());
            String name = getCategory(this.getSourceUrl() + categoryUrl);
            if (novelName.equals(name)) {
                List<String> newSections = Files.lines(Paths.get(categoryPath))
                        .collect(Collectors.toList());
                newSections.removeAll(sections);
                if (!newSections.isEmpty()) {
                    logger.info("发现新章节，章节数量：[{}]", newSections.size());
                    this.parseSectionList(newSections, novelName, true);
                } else {
                    logger.info("暂未发现新章节,更新完毕。");
                }
            } else {
                logger.error("追加文章名称与原文件名不同，原：[{}]，新：[{}]", novelName, name);
            }
        } catch (IOException e) {
            logger.error("读取目录文件失败：[{}]", categoryPath);
        }
    }

    private void parseSectionList(List<String> urls, String novelName, boolean append) {
        //查询目录表信息
        Map<String, List<String>> recordMap = new ConcurrentHashMap<>();
        List<String> retryUrl = new CopyOnWriteArrayList<>();
        Queue<String> sectionQueue = new LinkedList<>();
        SyncMission syncMission = new SyncMission.SyncMissionBuilder().executor(EXECUTOR).build();
        for (String line : urls) {
            sectionQueue.add(line);
            syncMission.addMission(() -> {
                try {
                    List<String> currentText = getDocument(SOURCE_URL + line);
                    recordMap.put(line, currentText);
                } catch (IOException e1) {
                    logger.error("读取章节地址失败:[{}] ", line);
                    retryUrl.add(line);
                }
            });
        }
        logger.info("章节任务添加完毕，等待执行任务池中的任务");
        syncMission.waitAllOf();
        //重试机制
        int breakTime = 0;
        while (breakTime < RETRY_MAX_TIMES) {
            logger.info("进入第[{}]次重试机制，当前重试url数量为:[{}]", breakTime + 1, retryUrl.size());
            if (retryUrl.size() == 0) {
                break;
            }
            for (int i = 0; i < retryUrl.size(); i++) {
                String item = retryUrl.get(i);
                try {
                    List<String> currentText = getDocument(SOURCE_URL + item);
                    recordMap.put(item, currentText);
                    retryUrl.remove(item);
                    i--;
                } catch (IOException e1) {
                    logger.error("重试读取章节地址失败:[{}] ", item);
                }
            }
            breakTime++;
        }
        //结果汇总处理
        List<String> result = new ArrayList<>();
        while (!sectionQueue.isEmpty()) {
            String line = sectionQueue.poll();
            List<String> list = recordMap.getOrDefault(line, Collections.singletonList(ERROR_MSG_PREFIX + SOURCE_URL + line));
            result.addAll(list);
        }
        //信息过滤
        List<String> terminalText = result.stream().filter(this::isRemain).collect(Collectors.toList());
        //结果输出文件
        Path sourcePath = Paths.get(BOOK_PATH + "/" + novelName + ".txt");
        Path retryPath = Paths.get(LOG_PATH + "/" + novelName + ".txt");
        try {
            if (!Files.exists(sourcePath)) {
                Files.createFile(sourcePath);
            }
            StandardOpenOption option = append ? StandardOpenOption.APPEND : StandardOpenOption.WRITE;
            Files.write(sourcePath, terminalText, option);
            if (!retryUrl.isEmpty()) {
                if (!Files.exists(retryPath)) {
                    Files.createFile(retryPath);
                }
                Files.write(retryPath, retryUrl, option);
            }
        } catch (IOException e) {
            logger.error("最终结果输出写入失败：文件写入失败或日志吸入失败");
            logger.error(e.getMessage());
        }
    }

    /**
     * @param sectionUrl 章节地址,即某一章的地址
     * @return java.util.List<java.lang.String> 章节文本列表，即某一章的内容文本
     * @author shijunhao
     * @date 2024/4/16 11:45
     */
    protected List<String> getDocument(String sectionUrl) throws IOException {
        Document document = Jsoup.parse(new URL(sectionUrl), 10000);
        return this.parseContent(document);
    }

    /**
     * @param categoryUrl 目录地址，即某本小说的目录地址
     * @return java.lang.String 文章名称 即 小说名称
     * @author shijunhao
     * @date 2024/4/16 11:45
     */
    protected String getCategory(String categoryUrl) {
        Document document;
        String renameTitle = null;
        try {
            document = Jsoup.parse(new URL(categoryUrl), 10000);
            List<String> category = this.parseCategory(document);
            String title = this.parseTitle(document);
            renameTitle = CommonUtil.removeSensitiveCharacters(title);
            Path file = Paths.get(CATEGORY_PATH + "/" + renameTitle + ".txt");
            Files.write(file, category);
            logger.info("获取本章目录成功：[{}]，共获取到[{}]页", renameTitle, category.size());
        } catch (IOException e) {
            logger.error("读取目录地址失败，目录地址[{}]", categoryUrl);
        }
        return renameTitle;
    }

    /**
     * @author shijunhao
     * @date 2024/5/15 11:33
     * 添加过滤规则
     */
    protected void addFilterRegex() {
        URL url;
        try {
            url = new URL(this.getSourceUrl());
            FILTER_REGEX_SET.add(url.getHost());
        } catch (MalformedURLException e) {
            logger.error("资源地址无法被解析");
        }
    }

    /**
     * @author shijunhao
     * @date 2024/5/15 12:52 初始化正则过滤列表
     */
    private void initPattern() {
        for (String regex : FILTER_REGEX_SET) {
            Pattern pattern = Pattern.compile(regex);
            FILTER_REGEX_PATTERN_SET.add(pattern);
        }
    }

    /**
     * @param line 文章段落
     * @return boolean 是否保留
     * 在过滤正则中的不保留，否则保留
     * @author shijunhao
     * @date 2024/5/15 11:38
     */
    private boolean isRemain(String line) {
        for (Pattern pattern : FILTER_REGEX_PATTERN_SET) {
            Matcher matcher = pattern.matcher(line);
            if (matcher.find()) {
                return false;
            }
        }
        return true;
    }

}
